﻿namespace Toolkit.Csv

open System
open System.IO
open System.Dynamic
open System.Collections.Generic
open System.Runtime.CompilerServices
open Parser
open System.Reflection
open Microsoft.FSharp.Reflection
open Toolkit.Reflection
open Toolkit.Collections
open System.Text.RegularExpressions

module CsvParser =
    
    // Covnerts a 2d array of strings to a sequence of dictionaries
    let private populateDictionaries (getResultInstance : Unit -> 'TResult) (add : 'TResult -> string * string -> 'TResult) (arr: string[][])=
        if arr = null || arr.Length = 0
        then failwith "The CSV format is incorrect; No rows or headers are found"
        else
            if arr.Length = 1
            then [||]
            else
                let headers = arr.[0]
                arr
                |> Seq.skip 1
                |> Seq.mapi (fun rowIdx row ->
                    if headers.Length <> row.Length 
                    then failwithf "The CSV format is incorrect; Expecting %d fields; Found %d at row %d" headers.Length row.Length rowIdx
                    else
                        (headers, row) 
                        ||> Seq.zip 
                        |> Seq.fold add (getResultInstance()))
                |> Seq.toArray 

    let private asDictionaries =
        populateDictionaries (fun () -> OrderedDictionary<string,string>()) (fun d (h,r) -> d.[h] <- r; d)
        >> Seq.cast<IDictionary<string,string>>
        >> Seq.toArray

    let private asDynamics = 
        populateDictionaries (fun () -> ExpandoObject()) (fun eo (h,r) -> (eo :> IDictionary<string,obj>).[h] <- r; eo) 
        >> Seq.cast<obj>
        >> Seq.toArray

    let private parseValue (prop: PropertyInfo) (str: string): obj =
        match prop.PropertyType with
        | ConvertibleType ct -> System.Convert.ChangeType(str, ct)
        | NullableType nt -> if str.Length = 0 then null else System.Convert.ChangeType(str, nt.GetGenericArguments().[0])
        | DateTimeOffsetType dtot ->
            match DateTimeOffset.TryParse str with
            | true, dto -> dto |> box
            | _ -> null
        | _ -> failwith ""

    let private rowToType<'TResult> (headers: string[])(row: string[]) : 'TResult=
        let ty = typeof<'TResult>
        let tobj = Activator.CreateInstance(ty) :?> 'TResult

        if headers.Length > row.Length then failwithf "Invalid row: %A" row

        for (propName, value) in (Seq.zip headers row) do
            if propName <> null then
                let prop = ty.GetProperty(propName)
                if prop.CanWrite then
                    try prop.SetValue(tobj, (parseValue prop value), null)
                    with e -> failwithf "Failed to parse field (%s : %s) from value=%s\n%s" propName (prop.PropertyType.Name) value (e.ToString())
        tobj

    let private asType<'TResult> (arr: string[][])=
        let ty = typeof<'TResult>

        // Fix column header formatting (remove spaces and other illegal characters for property names)
        let legalize (str: string) = Regex.Replace(str, "\\W", "")

        // Determine if headers are present
        let pnames = ty.GetProperties() |> Seq.map (fun p -> p.Name) |> Seq.toArray
        let firstRow = if arr.Length > 0 then Array.map legalize arr.[0] else null
        let hasHeaders = firstRow <> null &&  firstRow |> Seq.exists (fun header -> Seq.exists ((=) header) pnames)

        let headers = if hasHeaders 
                      then firstRow |> Seq.map (fun header -> if Seq.exists ((=) header) pnames then header else null) |> Seq.toArray
                      else pnames

        let rows = arr |> Seq.skip (if hasHeaders then 1 else 0)

        rows |> Seq.mapi (fun i row ->
                            try rowToType<'TResult> headers row
                            with
                            | e when not(hasHeaders) && i = 0 -> failwithf "%s at row %d; Check that headers match property names" e.Message i
                            | e -> failwithf "%s at row %d" e.Message i
                         )
             |> Seq.toArray
    
    let private parseReader(separator: char, reader : TextReader) = 
        Separator.current <- string(separator)
        let lexbuf = Lexing.LexBuffer<_>.FromTextReader(reader)
        Parser.start Lexer.tokenize lexbuf
        |> Seq.map Seq.toArray
        |> Seq.toArray

    let private parseText(separator, text) = 
        use reader = new StringReader(text)
        parseReader(separator, reader)

    // Methods for parsing text
    let ParseText(separator: char, text: string) = parseText(separator, text)

    let ParseTextAsDictionaries(separator: char, text: string) = ParseText(separator, text) |> asDictionaries

    let ParseTextAsDynamics(separator: char, text: string) : [<return: Dynamic([|false;true|])>] obj [] = ParseText(separator, text) |> asDynamics

    let ParseTextAs<'TResult>(separator: char, text: string) = parseText(separator, text) |> asType<'TResult>

    // Methods for parsing a stream
    let ParseStream(separator: char, stream: Stream) = 
        use reader = new StreamReader(stream)
        parseReader(separator, reader)

    let ParseStreamAsDictionaries(separator: char, stream: Stream) = ParseStream(separator, stream) |> asDictionaries

    let ParseStreamAsDynamics(separator: char, stream: Stream) : [<return: Dynamic([|false;true|])>] obj [] = ParseStream(separator, stream) |> asDynamics
    
    let ParseStreamAs<'TResult>(separator: char, stream: Stream) = ParseStream(separator, stream) |> asType<'TResult>

    // Methods for parsing file input
    let ParseFile(separator: char, filepath: string) = 
        use file = File.OpenRead(filepath)
        ParseStream(separator, file)

    let ParseFileAsDictionaries(separator: char, filepath: string) = ParseFile(separator, filepath) |> asDictionaries

    let ParseFileAsDynamics(separator: char, filepath: string) : [<return: Dynamic([|false;true|])>] obj [] = ParseFile(separator, filepath) |> asDynamics

    let ParseFileAs<'TResult>(separator: char, filepath: string) = ParseFile(separator, filepath) |> asType<'TResult>

    // Methods for parsing a TextReader
    let ParseReader(separator: char, reader: TextReader) = parseReader(separator, reader)
    
    let ParseReaderAsDictionaries(separator: char, reader: TextReader) = ParseReader(separator, reader) |> asDictionaries

    let ParseReaderAsDynamics(separator: char, reader: TextReader) = ParseReader(separator, reader) |> asDynamics

    let ParseReaderAs<'TResult>(separator: char, reader: TextReader) = ParseReader(separator, reader) |> asType<'TResult>